/*
 * @(#)StereoOscillator.java	1.0 Apr 26, 2008
 *
 *	The MIT License
 *
 *	Copyright (c) 2008 Malachi de AElfweald <malachid@gmail.com>
 *
 *	Permission is hereby granted, free of charge, to any person obtaining a copy
 *	of this software and associated documentation files (the "Software"), to deal
 *	in the Software without restriction, including without limitation the rights
 *	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *	copies of the Software, and to permit persons to whom the Software is
 *	furnished to do so, subject to the following conditions:
 *
 *	The above copyright notice and this permission notice shall be included in
 *	all copies or substantial portions of the Software.
 *
 *	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *	THE SOFTWARE.
 */
package org.eoti.sound;

import javax.sound.sampled.*;
import java.util.*;

public class StereoOscillator
{
	protected int frequencyL, frequencyR;
	protected int posL, posR;
	protected double amplitudeAdjL, amplitudeAdjR;
	protected WaveTableType type;
	protected int sampleRate, numberOfChannels;
	protected AudioFormat audioFormat;
	protected SourceDataLine lineOut;
	protected Channel channel;
	protected short[] waveTable;
	protected short[] tmp = new short[1];

	public StereoOscillator(
		WaveTableType type,
		int frequencyL,
		int frequencyR,
		int sampleRate
	){
		this.type = type;
		this.frequencyL = frequencyL;
		this.frequencyR = frequencyR;
		this.sampleRate = sampleRate;
		this.channel = Channel.Left;
		// if clipping occurs, reduce these to 0.7
		this.amplitudeAdjL = 1.0;
		this.amplitudeAdjR = 1.0;
		posL = 0;
		posR = 0;
		buildWaveTable();

		audioFormat = new AudioFormat(
			AudioFormat.Encoding.PCM_SIGNED, 	// Encoding
			sampleRate,							// sampleRate
			16, 								// sampleSizeInBits
			2, 									// channels
			4, 									// frameSize,
			sampleRate, 						// frameRate
			true 								// bigEndian
		);
	}

	public StereoOscillator(WaveTableType type, int frequencyL, int frequencyR,	SampleRate sampleRate)
	{this(type, frequencyL, frequencyR, sampleRate.getRate());}

	public StereoOscillator(int frequencyL, int frequencyR,	SampleRate sampleRate)
	{this(WaveTableType.SineWave, frequencyL, frequencyR, sampleRate);}

	public StereoOscillator(int frequencyL, int frequencyR,	int sampleRate)
	{this(WaveTableType.SineWave, frequencyL, frequencyR, sampleRate);}

	public StereoOscillator(int frequencyL, int frequencyR)
	{this(frequencyL, frequencyR, SampleRate.CD);}

	public StereoOscillator(int frequency)
	{this(frequency, frequency);}

	protected void buildWaveTable()
	{
		if(type != WaveTableType.NoType)
			waveTable = new short[sampleRate];// Allocate a table for 1 cycle

		switch(type)
		{
			case NoType:
				break;
			case Noise:
				buildNoiseWaveTable();
				break;
			case SineWave:
				buildSineWaveTable();
				break;
			case TriangleWave:
				buildTriangleWaveTable();
				break;
			case SquareWave:
				buildSquareWaveTable();
				break;
		}
	}

	protected void buildNoiseWaveTable()
	{
		/**
	     * Random number generator returns gaussian
	     * distributed numbers, resulting in approximation
	     * of white noise
	     */
	    Random rand = new Random(new Date().getTime());
	    for(int sample=0; sample<sampleRate; sample++)
	    	waveTable[sample] = (short)((65535.0 * rand.nextGaussian()) - 32768);
	}

	protected void buildSineWaveTable()
	{
		double scale = (2.0 * Math.PI) / sampleRate;
		for(int sample=0; sample<sampleRate; sample++)
			waveTable[sample] = (short)(32767.0 * Math.sin(sample * scale));
	}

	protected void buildTriangleWaveTable()
	{
		double sign = 1.0;
		double value = 0.0;

		int oneQuarterWave = sampleRate / 4;
		int threeQuarterWave = (3 * sampleRate) / 4;

		double scale = 32767.0 / oneQuarterWave;

		for(int sample=0; sample<sampleRate; sample++)
		{
			if((sample > oneQuarterWave) && (sample <= threeQuarterWave))
				sign = -1.0;
			else
				sign = 1.0;

			value += sign * scale;
			waveTable[sample] = (short)value;
		}
	}

	protected void buildSquareWaveTable()
	{
		int halfWave = sampleRate / 2;
		for(int sample=0; sample<sampleRate; sample++)
		{
			if(sample < halfWave)
				waveTable[sample] = 32767;
			else
				waveTable[sample] = -32768;
		}
	}

	public int read(byte[] buffer, int offs, int len)
	{
		int shortLen = 2 * len;
		if(tmp.length != shortLen)
			tmp = new short[shortLen];

		int numSamples = getSamples(tmp, shortLen);
		shortsToBytes(buffer, tmp, offs, numSamples/2, true);

		return len;
	}

	protected void shortsToBytes(
		byte[] data,
		short[] shorts,
		int offset,
		int len,
		boolean bigEndian
	){
		int numShorts = len / 2;
		int index = offset;
		for(int i=0; i<numShorts; i++)
		{
			short s = shorts[i];
			if(bigEndian)
			{
				data[index++] = (byte)(s >> 8);
				data[index++] = (byte)(s & 255);
			}else{
				data[index++] = (byte)(s & 255);
				data[index++] = (byte)(s >> 8);
			}
		}
	}

	protected int getSamples(short[] buffer, int length)
	{
		int sample = 0;
		int count = length;

		while((count--) != 0)
		{
			switch(channel)
			{
				case Left:
					buffer[sample++] = (short)(amplitudeAdjL * waveTable[posL]);
					posL += frequencyL;
					if(posL >= sampleRate)
						posL -= sampleRate;

					channel = Channel.Right;
					break;
				case Right:
					buffer[sample++] = (short)(amplitudeAdjR * waveTable[posR]);
					posR += frequencyR;
					if(posR >= sampleRate)
						posR -= sampleRate;

					channel = Channel.Left;
					break;
			}
		}

		return length;
	}

	public AudioFormat getAudioFormat()
	{return audioFormat;}

	public SourceDataLine getSourceDataLine()
	throws LineUnavailableException
	{
		if(lineOut == null)
		{
			DataLine.Info sourceLine = new DataLine.Info(SourceDataLine.class, audioFormat);
			lineOut = (SourceDataLine) AudioSystem.getLine(sourceLine);
		}

		return lineOut;
	}

	public void play()
	throws LineUnavailableException
	{
		SourceDataLine lineOut = getSourceDataLine();

		int bufferSize = (int)audioFormat.getSampleRate() * audioFormat.getFrameSize();
		lineOut.open(audioFormat, bufferSize);
		lineOut.start();

		// buffer for moving data from LineIn to LineOut
		byte[] buffer = new byte[bufferSize];

		try{
	    	int bytesRead = 0;
	    	while(bytesRead >= 0)
	    	{
	    		bytesRead = read(buffer, 0, buffer.length);
	    		if(bytesRead >= 0)
	    			lineOut.write(buffer, 0, bytesRead);
	    	}
	    }catch(Exception e){
	    	System.out.println("Exception: " + e.getMessage());
	    	e.printStackTrace();
	    }

	    lineOut.drain();
	    lineOut.close();
	}

	public static class Test
	{
		public static void main(String[] args)
		{
			if(args.length != 2)
			{
				System.out.println("USAGE: java org.eoti.sound.StereoOscillator$Test LEFT RIGHT");
				System.out.println("Where LEFT and RIGHT are frequencies (like 500, or 1000)");
				System.exit(0);
			}

			try{
		    	int freqL = Integer.parseInt(args[0]);
		    	int freqR = Integer.parseInt(args[1]);
				StereoOscillator lineIn = new StereoOscillator(freqL, freqR);
				lineIn.play();
		    }catch(Exception e){
		    	System.out.println("Exception: " + e.getMessage());
		    	e.printStackTrace();
		    }
		}
	}
}
